A/B-тест для мобильного приложения¶

Зачетный проект Яндекс Практикума по блоку тем «Анализ бизнес-показателей», «Событийная аналитика», «Принятие решений в бизнесе»

Цель: определить, влияет ли изменение шрифтов на поведение пользователей приложения.

Данные: лог с действиями пользователя и событиями.

Заказчик: команда стартапа.

Задачи:

  • EDA: проверка данных,
  • анализ воронки продаж,
  • статистический тест: сравнение контрольных групп А1 и А2 (проверка корректности работы инструмента разбиения пользователей на группы),
  • статистический тест: сравнение экпериментальной группы Б с контрольным (проверка влияния шрифтов).

Оглавление

  • 1 Обзор данных
    • 1.1 пропуски
    • 1.2 дубликаты
    • 1.3 добавление столбцов
    • 1.4 вывод
  • 2 Проверка данных
    • 2.1 соотношение пользователей и событий
    • 2.2 определение периода времени
    • 2.3 объемы групп
    • 2.4 вывод
  • 3 Анализ воронки событий
    • 3.1 распределение событий
    • 3.2 конверсия
    • 3.3 вывод
  • 4 Анализ А/А/В-теста
    • 4.1 подготовка данных
    • 4.2 проверка контрольных групп
    • 4.3 проверка экспериментальной группы
    • 4.4 вывод
  • 5 Итог

1 Обзор данных¶

In [1]:
import math as mth
import pandas as pd
import datetime as dt 
import scipy.stats as stats
from matplotlib import pyplot as plt
from plotly import graph_objects as go

pd.set_option('mode.chained_assignment', None)

Откроем датасет и сохраним данные в переменной.

In [2]:
data = pd.read_csv('logs_exp.csv', sep='\t')
    
display(data.head())
data.info()
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column          Non-Null Count   Dtype 
---  ------          --------------   ----- 
 0   EventName       244126 non-null  object
 1   DeviceIDHash    244126 non-null  int64 
 2   EventTimestamp  244126 non-null  int64 
 3   ExpId           244126 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 7.5+ MB

1.1 пропуски¶

Проверим данные на наличие пропусков.

In [3]:
data.isna().sum()
Out[3]:
EventName         0
DeviceIDHash      0
EventTimestamp    0
ExpId             0
dtype: int64

1.2 дубликаты¶

Проверим данные на наличие дубликатов.

In [4]:
print('Количество дубликатов:', data.duplicated().sum())
Количество дубликатов: 413

Дубликаты в данных есть. Проверим, какую часть они составляют.

In [5]:
print('Относительная потеря данных при удалении дубликатов: {0:.2%}'.format(data.duplicated().sum() / len(data)))
Относительная потеря данных при удалении дубликатов: 0.17%

Доля составляет менее 0,2%. Удалим дубликаты.

In [6]:
data = data.drop_duplicates()

1.3 добавление столбцов¶

Передадим столбцам более короткие названия.

In [7]:
data.columns = ['event', 'id', 'timestamp', 'group']

Добавим новые столбцы: с датой и временем, с датой.

In [8]:
data['datetime'] = pd.to_datetime(data['timestamp'], unit='s')
data['date'] = data['datetime'].dt.date

1.4 вывод¶

Посмотрим на получившуюся таблицу.

In [9]:
display(data.head())
data.info()
event id timestamp group datetime date
0 MainScreenAppear 4575588528974610257 1564029816 246 2019-07-25 04:43:36 2019-07-25
1 MainScreenAppear 7416695313311560658 1564053102 246 2019-07-25 11:11:42 2019-07-25
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
3 CartScreenAppear 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248 2019-07-25 11:48:42 2019-07-25
<class 'pandas.core.frame.DataFrame'>
Int64Index: 243713 entries, 0 to 244125
Data columns (total 6 columns):
 #   Column     Non-Null Count   Dtype         
---  ------     --------------   -----         
 0   event      243713 non-null  object        
 1   id         243713 non-null  int64         
 2   timestamp  243713 non-null  int64         
 3   group      243713 non-null  int64         
 4   datetime   243713 non-null  datetime64[ns]
 5   date       243713 non-null  object        
dtypes: datetime64[ns](1), int64(3), object(2)
memory usage: 13.0+ MB

Данные сохранены в переменной, пропусков не обнаружено, дубликаты удалены. Добавлены новые столбцы с датой и временем, а также отдельно с датой.

назад в оглавление

2 Проверка данных¶

2.1 соотношение пользователей и событий¶

Посмотрим, какие события зафиксированы в логе и сколько в логе уникальных пользователей.

In [10]:
display(data['event'].unique())
print('Всего видов событий:', len(data['event'].unique()))
print('Уникальных пользователей:', len(data['id'].unique()))
array(['MainScreenAppear', 'PaymentScreenSuccessful', 'CartScreenAppear',
       'OffersScreenAppear', 'Tutorial'], dtype=object)
Всего видов событий: 5
Уникальных пользователей: 7551

Посчитаем среднее количество событий на пользователя. Для этого вначале определим, есть ли выбросы в данных. Построим графики.

In [11]:
fig, ax = plt.subplots(1, 2, figsize=(12, 5))
plt.suptitle('Распределение количества событий на пользователя')
data.groupby('id').agg({'event': 'count'}).plot(kind='box', ax=ax[0], ylabel='общее число событий')
data.groupby('id').agg({'event': 'nunique'}).plot(kind='box', ax=ax[1], ylabel='число уникальных событий');

В общем количестве событий на пользователя есть значительные выбросы. В этом случае лучше возьмем медиану. В количестве уникальных событий на пользователя выбросов нет, можно использовать среднее, эта метрика будет более точной.

In [12]:
print('Медианное количество событий на пользователя:', 
      round(data.groupby('id').agg({'event': 'count'}).median()['event'], 2))
print('Среднее количество уникальных событий на пользователя:', 
      round(data.groupby('id').agg({'event': 'nunique'}).mean()['event'], 2))
Медианное количество событий на пользователя: 20.0
Среднее количество уникальных событий на пользователя: 2.67

2.2 определение периода времени¶

Найдем максимальную и минимальную даты в логе.

In [13]:
print('Начало наблюдений:', data['date'].min())
print('Окончание наблюдений:', data['date'].max())
Начало наблюдений: 2019-07-25
Окончание наблюдений: 2019-08-07

Построим гистограмму по дате и времени.

In [14]:
data['datetime'].hist(figsize=(12, 3), bins=200)
plt.title('Распределение данных по дате и времени')
plt.ylabel('кол-во событий')
plt.xlabel('дата')
plt.xticks(list(data['date'].unique()), rotation=20);

Данные до 1 августа очевидно не полные, поэтому их не нужно брать для анализа. Остается определить границу по времени между датами. Предположим, что если мы возьмем время окончания наблюдений и отсчитаем определенное количество полных суток в обратную сторону, то получим полный датасет.

In [15]:
end = data['datetime'].max().to_pydatetime()
print('Окончание наблюдений:', end)
Окончание наблюдений: 2019-08-07 21:15:17

Итак, видим, что для получения 7 полных суток нужно взять данные с 9 вечера 31 июля. Другим решением будет провести границу по полуночи 1 августа. Построим гистограмму в границах, определенных по первому варианту.

In [16]:
start = end - dt.timedelta(days=7)

data.loc[data['datetime'] > start]['datetime'].hist(figsize=(12, 3), bins=200)
plt.title('Распределение данных по дате и времени в актуальный период')
plt.ylabel('кол-во событий')
plt.xlabel('дата');

По полученному графику видим, что за 3 часа от 31 июля у нас действительно имеется значимое количество данных и в выбранный период попадает первый всплеск данных, видимый на предыдущем графике. Принимаем решение взять данные за 7 полных суток.

In [17]:
dt = data.loc[data['datetime'] > start]
dt.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 241657 entries, 2057 to 244125
Data columns (total 6 columns):
 #   Column     Non-Null Count   Dtype         
---  ------     --------------   -----         
 0   event      241657 non-null  object        
 1   id         241657 non-null  int64         
 2   timestamp  241657 non-null  int64         
 3   group      241657 non-null  int64         
 4   datetime   241657 non-null  datetime64[ns]
 5   date       241657 non-null  object        
dtypes: datetime64[ns](1), int64(3), object(2)
memory usage: 12.9+ MB

Проверим, какое количество событий и пользователей мы теряем, отбросив старые данные.

In [18]:
print(
    'Количество потерянных уникальных пользователей:',
    len(data['id'].unique()) - len(dt['id'].unique()), 
    '\n',
    'Относительная потеря данных: {0:.2%}'
    .format((len(data['id'].unique()) - len(dt['id'].unique())) / len(data['id'].unique())),
    '\n'
)
print('Количество потерянных событий:',
    len(data['event']) - len(dt['event']),
    '\n',
    'Относительная потеря данных: {0:.2%}'
    .format((len(data['event']) - len(dt['event'])) / len(data['event']))
)
Количество потерянных уникальных пользователей: 13 
 Относительная потеря данных: 0.17% 

Количество потерянных событий: 2056 
 Относительная потеря данных: 0.84%

Относительные потери данных незначительны. Посмотрим, как изменилось среднее количество событий на пользователя.

In [19]:
print('Медианное количество событий на пользователя:', 
      round(dt.groupby('id').agg({'event': 'count'}).median()['event'], 2))
print('Среднее количество уникальных событий на пользователя:', 
      round(dt.groupby('id').agg({'event': 'nunique'}).mean()['event'], 2))
Медианное количество событий на пользователя: 19.0
Среднее количество уникальных событий на пользователя: 2.67

Среднее и медианное количество событий почти не изменилось.

2.3 объемы групп¶

Проверим, что в данных за актуальный период попали пользователи из всех трёх экспериментальных групп.

In [20]:
groups = dt.groupby('group').agg(count=('timestamp', 'count'))
groups['share'] = round(groups['count'] / len(dt), 2)
groups
Out[20]:
count share
group
246 79533 0.33
247 77277 0.32
248 84847 0.35

2.4 вывод¶

Всего в датасете уникальных видов событий — 5, в среднем приходится по 2,67 уникальных события на пользователя.

Определены границы актуального периода: с 9 вечера 31 июля до 9 вечера 8 августа.

После отсечения старых данных относительные потери в данных составляют меньше 1%.

В данных за актуальный период присутствуют пользователи из всех трех экспериментальных групп, объемы групп сопоставимы по размеру.

назад в оглавление

3 Анализ воронки событий¶

3.1 распределение событий¶

Посмотрим на общую частоту событий в датасете.

In [21]:
dt.groupby('event')['id'].count().sort_values(ascending=False)
Out[21]:
event
MainScreenAppear           117853
OffersScreenAppear          46514
CartScreenAppear            42336
PaymentScreenSuccessful     33945
Tutorial                     1009
Name: id, dtype: int64

Посчитаем, сколько уникальных пользователей совершали каждое из этих событий и посчитаем долю пользователей, которые хоть раз совершали событие.

In [22]:
(dt
 .groupby('event')
 .agg(users=('id', 'nunique'), share=('id', lambda x: x.nunique() / dt['id'].nunique()))
 .reset_index().sort_values('users', ascending=False)
)
Out[22]:
event users share
1 MainScreenAppear 7423 0.984744
2 OffersScreenAppear 4596 0.609711
0 CartScreenAppear 3736 0.495622
3 PaymentScreenSuccessful 3540 0.469621
4 Tutorial 843 0.111833

В целом частота событий распределяется согласно логике воронки: главный экран — экран товара — ввод данных карты — успешность проведения оплаты. Однако тот факт, что доля уникальных пользователей в первом событии в этой цепочке не 100%, говорит о том, что часть пользователей начинала с других шагов. Вероятно, в приложении есть возможность начать действия сразу со страницы товара, перейдя по прямой ссылке.

Под Tutorial, скорее всего, подразумевается обучение использованию приложения. Решим, что так, тогда это событие не является звеном последовательной цепочки. Исключим логи с ним для последующего анализа воронки.

In [23]:
fun = dt.loc[dt['event'] != 'Tutorial']

(fun
 .groupby('event')
 .agg(users=('id', 'nunique'), share=('id', lambda x: x.nunique() / fun['id'].nunique()))
 .reset_index().sort_values('users', ascending=False)
)
Out[23]:
event users share
1 MainScreenAppear 7423 0.985267
2 OffersScreenAppear 4596 0.610035
0 CartScreenAppear 3736 0.495885
3 PaymentScreenSuccessful 3540 0.469870

Оценим объем потери данных после исключения логов с Tutorial.

In [24]:
print(
    'Количество потерянных уникальных пользователей:',
    len(dt['id'].unique()) - len(dt.loc[dt['event'] != 'Tutorial']['id'].unique()), 
    '\n',
    'Относительная потеря данных: {0:.2%}'
    .format((len(dt['id'].unique()) - len(dt.loc[dt['event'] != 'Tutorial']['id'].unique())) / len(dt['id'].unique())),
    '\n'

)
print('Количество потерянных логов с событиями:',
    len(dt['event']) - len(dt.loc[dt['event'] != 'Tutorial']['event']),
    '\n',
    'Относительная потеря данных: {0:.2%}'
    .format((len(dt['event']) - len(dt.loc[dt['event'] != 'Tutorial']['event'])) / len(dt['event']))
)
Количество потерянных уникальных пользователей: 4 
 Относительная потеря данных: 0.05% 

Количество потерянных логов с событиями: 1009 
 Относительная потеря данных: 0.42%

Относительные потери данных незначительны.

3.2 конверсия¶

По обновленному датасету посчитаем, какая доля пользователей переходит на каждый следующий шаг воронки.

In [25]:
steps = fun.groupby('event').agg(users=('id', 'nunique')).sort_values(by='users', ascending=False)
steps['step_share'] = steps['users'] / steps['users'].shift(1)

steps
Out[25]:
users step_share
event
MainScreenAppear 7423 NaN
OffersScreenAppear 4596 0.619157
CartScreenAppear 3736 0.812881
PaymentScreenSuccessful 3540 0.947537

По полученной таблице видим, что наибольшее количество пользователей теряется на втором шаге. Доли тех, кто перешел от страницы товара к вводу данных карты и далее увидел экран успешной оплаты — значительно выше.

In [26]:
funnel = (
    fun
    .groupby('event')
    .agg(users=('id', 'nunique'), share=('id', lambda x: x.nunique() / fun['id'].nunique()))
    .reset_index().sort_values('users', ascending=False)
)

fig = go.Figure(go.Funnel(
    y = list(funnel['event']),
    x = list(funnel['users']),
    textinfo = 'value+percent initial+percent previous')
               )
fig.update_layout(title='Доля перехода пользователей на каждый следующий шаг воронки', title_x = 0.5)
fig.show()

3.3 вывод¶

Цепочка событий в воронке выглядит следующим образом: главный экран — экран товара — ввод данных карты — успешность проведения оплаты.

Событие Tutorial не входит в цепочку.

После удаления логов с событием Tutorial относительные потери данных составляют меньше 0,5%.

Больше всего пользователей теряется на переходе ко второму шагу — от главного экрана к странице товара. Однако есть еще потери, на которые следует обратить внимание: от ввода данных карты до экрана успешной оплаты переходят около 95% пользователей. Необходимо проверить в последующем, не связано ли это с техническими проблемами.

От первого события до оплаты доходит 48% уникальных пользователей.

назад в оглавление

4 Анализ А/А/В-теста¶

4.1 подготовка данных¶

Переименуем группы для удобства работы.

In [27]:
fun['group'] = fun['group'].replace([246, 247, 248], ['A1', 'A2', 'B'])

Проверим, не пересекаются ли пользователи в группах.

In [28]:
group_a1 = list(fun.query('group == "A1"')['id'])
group_a2 = list(fun.query('group == "A2"')['id'])

print('Пользователей из группы A1 в других группах:', 
      len(fun.query('group != "A1"').query('id.isin(@group_a1)')['id'].unique()))
print('Пользователей из группы A2 в других группах:', 
      len(fun.query('group != "A2"').query('id.isin(@group_a2)')['id'].unique()))
Пользователей из группы A1 в других группах: 0
Пользователей из группы A2 в других группах: 0

Посчитаем количество пользователей в каждой экспериментальной группе.

In [29]:
group_size = fun.groupby('group').agg(users=('id', 'nunique'))
group_size
Out[29]:
users
group
A1 2483
A2 2516
B 2535

Посчитаем количество пользователей в каждой группе на каждом шаге.

In [30]:
test = (
    fun
    .pivot_table(index='event', columns='group', values='id', aggfunc='nunique')
    .sort_values(by='B', ascending=False)
)
test
Out[30]:
group A1 A2 B
event
MainScreenAppear 2450 2479 2494
OffersScreenAppear 1542 1523 1531
CartScreenAppear 1266 1239 1231
PaymentScreenSuccessful 1200 1158 1182

Напишем функцию для проведения z-теста.

In [31]:
def z_test(event_1, event_2, total_1, total_2):
    
    p1 = event_1 / total_1
    p2 = event_2 / total_2
    
    p_combined = (event_1 + event_2) / (total_1 + total_2)
    
    diff = p1 - p2
    
    z_value = diff / mth.sqrt(p_combined * (1 - p_combined) * (1/total_1 + 1/total_2))
    distr = stats.norm(0, 1)  
    
    p_value = (1 - distr.cdf(abs(z_value))) * 2
    
    return p_value

4.2 проверка контрольных групп¶

Проверим, находят ли статистические критерии разницу между контрольными группами A1 и A2 на каждом шаге. Используем z-тест пропорций.

Гипотезы для каждого из шагов:

H_0: Конверсия в группах А1 и А2 одинакова.
H_a: Конверсия в группах А1 и А2 не одинакова.

Установим параметр alpha — 0,05.

Поскольку на одних данных проводится несколько сравнений, мы имеем дело с множественным тестом: с каждой новой проверкой гипотезы растёт вероятность ошибки первого рода. С учетом этого применим поправку Холма.

In [32]:
alpha = 0.05
quantity = 4

for i in range(len(test)):
    p_value = z_test(test.iloc[i, 0], test.iloc[i, 1], group_size.iloc[0], group_size.iloc[1])
    
    print(f'Событие {test.index[i]}, p-значение: {p_value}')
    print('Уровень значимости:', alpha / quantity)
    
    if p_value < alpha / quantity:
        print('Отвергаем нулевую гипотезу')
    else:
        print('Не получилось отвергнуть нулевую гипотезу')
    print('\n')
    
    quantity -= 1
Событие MainScreenAppear, p-значение: [0.67020827]
Уровень значимости: 0.0125
Не получилось отвергнуть нулевую гипотезу


Событие OffersScreenAppear, p-значение: [0.25455454]
Уровень значимости: 0.016666666666666666
Не получилось отвергнуть нулевую гипотезу


Событие CartScreenAppear, p-значение: [0.21811884]
Уровень значимости: 0.025
Не получилось отвергнуть нулевую гипотезу


Событие PaymentScreenSuccessful, p-значение: [0.10288527]
Уровень значимости: 0.05
Не получилось отвергнуть нулевую гипотезу


P-значение в каждом из случаев значительно превышает alpha (превышало бы даже без поправки), что позволяет сделать вывод о том, что по статистическим критериям разницы между контрольными группами нет. Можно утверждать, что разбиение на группы проведено корректно.

4.3 проверка экспериментальной группы¶

Подобным образом проведем проверку для экспериментальной группы B (с измененным шрифтом) по каждому событию:

1) с каждой из контрольных групп в отдельности (8 сравнений), 
2) с объединенной контрольной группой (4 сравнения).

Дополним датасет новыми столбцами с данными для объединенной контрольной группы, а также столбцами с размерами групп.

In [33]:
test.insert(2, 'AA', test['A1'] + test['A2'])
test['A1_size'] = group_size.iloc[0, 0]
test['A2_size'] = group_size.iloc[1, 0]
test['AA_size'] = test['A1_size'] + test['A2_size']
test['B_size'] = group_size.iloc[2, 0]
test
Out[33]:
group A1 A2 AA B A1_size A2_size AA_size B_size
event
MainScreenAppear 2450 2479 4929 2494 2483 2516 4999 2535
OffersScreenAppear 1542 1523 3065 1531 2483 2516 4999 2535
CartScreenAppear 1266 1239 2505 1231 2483 2516 4999 2535
PaymentScreenSuccessful 1200 1158 2358 1182 2483 2516 4999 2535

Аналогично применим z-тест для сравнения долей в каждом из 12 случаев. Гипотезы для каждого из шагов:

H_0: Конверсия в группах одинакова.
H_a: Конверсия в группах не одинакова.

Установим параметр alpha — 0,05. С учетом множественности теста также применим поправку Холма.

In [34]:
quantity = 12

for y in range(0, 3):
    print(f'Сравнение групп {test.columns[y]} и {test.columns[3]}', '\n')
    for i in range(len(test)):

        p_value = z_test(test.iloc[i, y], test.iloc[i, 3], test.iloc[i, y+4], test.iloc[i, 7])

        print(f'Событие {test.index[i]}, p-значение: {p_value}')
        print('Уровень значимости:', alpha / quantity)

        if p_value < alpha / quantity:
            print('Отвергаем нулевую гипотезу')
        else:
            print('Не получилось отвергнуть нулевую гипотезу')
        print('\n')
        
        quantity -= 1
Сравнение групп A1 и B 

Событие MainScreenAppear, p-значение: 0.396910049618151
Уровень значимости: 0.004166666666666667
Не получилось отвергнуть нулевую гипотезу


Событие OffersScreenAppear, p-значение: 0.21442476639710506
Уровень значимости: 0.004545454545454546
Не получилось отвергнуть нулевую гипотезу


Событие CartScreenAppear, p-значение: 0.08564271892834707
Уровень значимости: 0.005
Не получилось отвергнуть нулевую гипотезу


Событие PaymentScreenSuccessful, p-значение: 0.22753674585530037
Уровень значимости: 0.005555555555555556
Не получилось отвергнуть нулевую гипотезу


Сравнение групп A2 и B 

Событие MainScreenAppear, p-значение: 0.6723167704766229
Уровень значимости: 0.00625
Не получилось отвергнуть нулевую гипотезу


Событие OffersScreenAppear, p-значение: 0.9200426006644042
Уровень значимости: 0.0071428571428571435
Не получилось отвергнуть нулевую гипотезу


Событие CartScreenAppear, p-значение: 0.6264599792848009
Уровень значимости: 0.008333333333333333
Не получилось отвергнуть нулевую гипотезу


Событие PaymentScreenSuccessful, p-значение: 0.6680367850275775
Уровень значимости: 0.01
Не получилось отвергнуть нулевую гипотезу


Сравнение групп AA и B 

Событие MainScreenAppear, p-значение: 0.4599468774918498
Уровень значимости: 0.0125
Не получилось отвергнуть нулевую гипотезу


Событие OffersScreenAppear, p-значение: 0.4402711073657435
Уровень значимости: 0.016666666666666666
Не получилось отвергнуть нулевую гипотезу


Событие CartScreenAppear, p-значение: 0.20361356481451098
Уровень значимости: 0.025
Не получилось отвергнуть нулевую гипотезу


Событие PaymentScreenSuccessful, p-значение: 0.6559128929243401
Уровень значимости: 0.05
Не получилось отвергнуть нулевую гипотезу


Ни в одном из случаев сравнения — группы A1 и B, группы A2 и B, группы AA объединенная и B — и ни на одном из шагов не удалось обнаружить статистически значимой разницы между долями. Во всех случаях значение p-value не приближается к пороговому значению alpha даже без поправки.

4.4 вывод¶

Пересечения пользователей во всех трех группах не выявлено. Общие размеры всех трех групп сопоставимы.

Между контрольными группами A1 и A2 разницы по статистическим критериям не выявлено. Можно утверждать, что разбиение на группы проведено корректно.

Для экспериментальной группы B не выявлено разницы по статистическим критериям ни в одном из случаев сравнения:

  • с контрольной группой A1,
  • с контрольной группой A2,
  • с объединенной контрольной группой AA.

назад в оглавление

5 Итог¶

  1. Границы актуального периода составляют 7 полных суток — с 9 вечера 31 июля по 9 вечера 8 августа.
  1. В среднем на пользователя приходится по 2,67 уникальных события (всего в логе 5 видов событий).
  1. Событийную воронку составляют 4 события: главный экран — экран товара — ввод данных карты — успешность проведения оплаты. Событие Tutorial не входит в цепочку.
  1. В сравнительном отношении по шагам больше всего пользователей теряется на переходе ко второму шагу — от главного экрана к странице товара переходит 62% пользователей.
  1. Следует обратить внимание на то, что от ввода данных карты до экрана успешной оплаты переходят около 95% пользователей. Возможно, нужно проверить, не связано ли это с техническими проблемами.
  1. От первого события до оплаты доходит около 47% уникальных пользователей.
  1. Пересечения пользователей во всех трех группах не выявлено. Объемы всех трех групп сопоставимы по размеру. Между контрольными группами A1 и A2 разницы по статистическим критериям не выявлено. Разбиение на группы проведено корректно.
  1. Для экспериментальной группы B не выявлено разницы по статистическим критериям ни в одном из случаев сравнения с контрольными группами.

Подводим итог эксперимента: изменение шрифтов в приложении не влияет на поведение пользователей.

назад в оглавление